/**
 * An internally used DataView for {@link Ext.form.field.ComboBox ComboBox}.
 */
Ext.define('Ext.view.BoundList', {
    extend: 'Ext.view.View',
    alias: 'widget.boundlist',
    alternateClassName: 'Ext.BoundList',

    requires: [
        'Ext.view.BoundListKeyNav',
        'Ext.layout.component.BoundList',
        'Ext.toolbar.Paging'
    ],

    mixins: [
        'Ext.mixin.Queryable'
    ],

    /**
     * @cfg {Number} pageSize
     * If greater than `0`, a {@link Ext.toolbar.Paging} is displayed at the bottom of the list
     * and store queries will execute with page {@link Ext.data.operation.Read#start start} and
     * {@link Ext.data.operation.Read#limit limit} parameters.
     */
    pageSize: 0,

    /**
     * @cfg {String} [displayField=""]
     * The field from the store to show in the view.
     */

    /**
     * @property {Ext.toolbar.Paging} pagingToolbar
     * A reference to the PagingToolbar instance in this view. Only populated if {@link #pageSize}
     * is greater than zero and the BoundList has been rendered.
     */

    /**
     * @cfg baseCls
     * @inheritdoc
     */
    baseCls: Ext.baseCSSPrefix + 'boundlist',

    /**
     * @cfg itemCls
     * @inheritdoc
     */
    itemCls: Ext.baseCSSPrefix + 'boundlist-item',
    listItemCls: '',

    /**
     * @cfg shadow
     * @inheritdoc
     */
    shadow: false,

    /**
     * @cfg trackOver
     * @inheritdoc
     */
    trackOver: true,

    /**
     * @cfg preserveScrollOnRefresh
     * @inheritdoc
     */
    preserveScrollOnRefresh: true,
    enableInitialSelection: false,
    refreshSelmodelOnRefresh: true,

    /**
     * @cfg componentLayout
     * @inheritdoc
     */
    componentLayout: 'boundlist',

    /**
     * @cfg navigationModel
     * @inheritdoc
     */
    navigationModel: 'boundlist',

    /**
     * @cfg scrollable
     * @inheritdoc
     */
    scrollable: true,

    /**
     * @property ariaEl
     * @inheritdoc
     */
    ariaEl: 'listEl',

    /**
     * @cfg tabIndex
     * @inheritdoc
     */
    tabIndex: -1,

    /**
     * @cfg childEls
     * @inheritdoc
     */
    childEls: [
        'listWrap', 'listEl'
    ],

    /* eslint-disable indent */
    /**
     * @cfg renderTpl
     * @inheritdoc
     */
    renderTpl: [
        '<div id="{id}-listWrap" data-ref="listWrap"',
                ' class="{baseCls}-list-ct ', Ext.dom.Element.unselectableCls, '">',
            '<ul id="{id}-listEl" data-ref="listEl" class="', Ext.baseCSSPrefix, 'list-plain"',
                '<tpl foreach="ariaAttributes"> {$}="{.}"</tpl>',
            '>',
            '</ul>',
        '</div>',
        '{%',
            'var pagingToolbar=values.$comp.pagingToolbar;',
            'if (pagingToolbar) {',
                'Ext.DomHelper.generateMarkup(pagingToolbar.getRenderTree(), out);',
            '}',
        '%}',
        {
            disableFormats: true
        }
    ],
    /* eslint-enable indent */

    /**
     * @cfg {String/Ext.XTemplate} tpl
     * A String or Ext.XTemplate instance to apply to inner template.
     *
     * {@link Ext.view.BoundList} is used for the dropdown list of 
     * {@link Ext.form.field.ComboBox}. To customize the template you can set the tpl on 
     * the combobox config object:
     *
     *     Ext.create('Ext.form.field.ComboBox', {
     *         fieldLabel   : 'State',
     *         queryMode    : 'local',
     *         displayField : 'text',
     *         valueField   : 'abbr',
     *         store        : Ext.create('StateStore', {
     *             fields : ['abbr', 'text'],
     *             data   : [
     *                 {"abbr":"AL", "name":"Alabama"},
     *                 {"abbr":"AK", "name":"Alaska"},
     *                 {"abbr":"AZ", "name":"Arizona"}
     *                 //...
     *             ]
     *         }),
     *         // Template for the dropdown menu.
     *         // Note the use of the "x-list-plain" and "x-boundlist-item" class,
     *         // this is required to make the items selectable.
     *         tpl: Ext.create('Ext.XTemplate',
     *             '<ul class="x-list-plain"><tpl for=".">',
     *                 '<li role="option" class="x-boundlist-item">{abbr} - {name}</li>',
     *             '</tpl></ul>'
     *         ),
     *     });
     *
     * Defaults to:
     *
     *     Ext.create('Ext.XTemplate',
     *         '<ul><tpl for=".">',
     *             '<li role="option" class="' + itemCls + '">' + me.getInnerTpl(me.displayField) +
     *             '</li>',
     *         '</tpl></ul>'
     *     );
     *
     */

    // Override because on non-touch devices, the bound field
    // retains focus so that typing may narrow the list.
    // Only when the show is triggered by a touch does the BoundList
    // get explicitly focused so that the keyboard does not appear.
    /**
     * @cfg focusOnToFront
     * @inheritdoc
     */
    focusOnToFront: false,

    /**
     * @cfg alignOnScroll
     * @inheritdoc
     */
    alignOnScroll: false,

    initComponent: function() {
        var me = this,
            baseCls = me.baseCls,
            itemCls = me.itemCls;

        me.selectedItemCls = baseCls + '-selected';

        if (me.trackOver) {
            me.overItemCls = baseCls + '-item-over';
        }

        me.itemSelector = '.' + itemCls;

        if (me.floating) {
            me.addCls(baseCls + '-floating');
        }

        if (!me.tpl) {
            // should be setting aria-posinset based on entire set of data
            // not filtered set
            me.generateTpl();
        }
        else if (!me.tpl.isTemplate) {
            me.tpl = new Ext.XTemplate(me.tpl);
        }

        if (me.pageSize) {
            me.pagingToolbar = me.createPagingToolbar();
        }

        me.callParent();
    },

    /**
     * Allow tpl to be generated programmatically to respond to changes in displayField
     * @private
     */
    generateTpl: function() {
        var me = this;

        /* eslint-disable indent, max-len */
        me.tpl = new Ext.XTemplate(
            '<tpl for=".">',
                '<li role="option" unselectable="on" class="' + me.itemCls + '">' + me.getInnerTpl(me.displayField) + '</li>',
            '</tpl>'
        );
        /* eslint-enable indent, max-len */
    },

    /**
     * Updates the display field for this view. This will automatically trigger
     * an regeneration of the tpl so that the updated displayField can be used
     * @param {String} displayField
     */
    setDisplayField: function(displayField) {
        this.displayField = displayField;
        this.generateTpl();
    },

    getRefOwner: function() {
        return this.pickerField || this.callParent();
    },

    getRefItems: function() {
        var result = this.callParent(),
            toolbar = this.pagingToolbar;

        if (toolbar) {
            result.push(toolbar);
        }

        return result;
    },

    createPagingToolbar: function() {
        var me = this;

        return new Ext.toolbar.Paging({
            id: me.id + '-paging-toolbar',
            pageSize: me.pageSize,
            store: me.dataSource,
            border: false,
            ownerCt: me,
            ownerLayout: me.getComponentLayout()
        });
    },

    refresh: function() {
        var me = this,
            tpl = me.tpl;

        // Allow access to the context for XTemplate scriptlets
        tpl.field = me.pickerField;
        tpl.store = me.store;
        me.callParent();
        tpl.field = tpl.store = null;

        if (!me.ariaStaticRoles[me.ariaRole]) {
            me.refreshAriaAttributes();
        }

        // The view selectively removes item nodes, so the toolbar
        // will be preserved in the DOM
    },

    refreshAriaAttributes: function() {
        var me = this,
            store = me.store,
            selModel = me.getSelectionModel(),
            multiSelect, totalCount, nodes, node, record, index, i, len;

        // When the store is filtered or paged, we want to let the Assistive Technology
        // users know that there are more records than currently displayed. This is not
        // a requirement when the whole dataset fits the DOM.
        // Note that it is possible for the store to be filtered but not fit the DOM.
        // In that case we use filtered count as the set size.
        totalCount = store.isFiltered()
            ? store.getCount()
            : store.getTotalCount() || store.getCount();

        nodes = me.getNodes();

        multiSelect = me.pickerField && me.pickerField.multiSelect;

        for (i = 0, len = nodes.length; i < len; i++) {
            node = nodes[i];
            record = null;

            if (totalCount !== len) {
                record = me.getRecord(node);
                index = store.indexOf(record);

                node.setAttribute('aria-setsize', totalCount);
                node.setAttribute('aria-posinset', index);
            }

            // For single-select combos aria-selected must be undefined
            if (multiSelect) {
                record = record || me.getRecord(node);
                node.setAttribute('aria-selected', selModel.isSelected(record));
            }
        }
    },

    bindStore: function(store, initial) {
        var toolbar = this.pagingToolbar;

        this.callParent(arguments);

        if (toolbar) {
            toolbar.bindStore(store, initial);
        }
    },

    /**
     * A method that returns the inner template for displaying items in the list.
     * This method is useful to override when using a more complex display value, for example
     * inserting an icon along with the text.
     *
     * The XTemplate is created with a reference to the owning form field in the `field` property
     * to provide access to context. For example to highlight the currently typed value,
     * the getInnerTpl may be configured into a ComboBox as part of the
     * {@link Ext.form.field.ComboBox#listConfig listConfig}:
     *
     *     listConfig: {
     *         getInnerTpl: function() {
     *             return '{[values.name.replace(this.field.getRawValue(), "<b>" +
     *                    this.field.getRawValue() + "</b>")]}';
     *         }
     *     }
     * @param {String} displayField The {@link #cfg!displayField} for the BoundList.
     * @return {String} The inner template
     */
    getInnerTpl: function(displayField) {
        return '{' + displayField + '}';
    },

    onShow: function() {
        var field = this.pickerField;

        this.callParent();

        // If the input field is not focused, then focus it.
        if (field && field.rendered && !field.hasFocus && !Ext.isTouchMode()) {
            field.focus();
        }
    },

    afterComponentLayout: function(width, height, oldWidth, oldHeight) {
        var field = this.pickerField;

        this.callParent([width, height, oldWidth, oldHeight]);

        // Bound list may change size, so realign on layout
        // **if the field is an Ext.form.field.Picker which has alignPicker!**
        if (field && field.alignPicker) {
            field.alignPicker();
        }
    },

    onItemSelect: function(record) {
        var me = this,
            node;

        node = me.callParent([record]);

        if (node) {
            if (me.ariaSelectable) {
                node.setAttribute('aria-selected', 'true');
            }
            else {
                node.removeAttribute('aria-selected');
            }
        }

        return node;
    },

    onItemDeselect: function(record) {
        var me = this,
            node;

        node = me.callParent([record]);

        if (node && me.ariaSelectable) {
            if (me.pickerField && me.pickerField.multiSelect) {
                node.setAttribute('aria-selected', 'false');
            }
            else {
                node.removeAttribute('aria-selected');
            }
        }

        return node;
    },

    // Clicking on an already selected item collapses the picker
    onItemClick: function(record) {
        // The selection change events won't fire when clicking on the selected element.
        // Detect it here.
        var me = this,
            field = me.pickerField,
            valueField, selected;

        if (!field) {
            return;
        }

        valueField = field.valueField;
        selected = me.getSelectionModel().getSelection();

        if (!field.multiSelect && selected.length) {
            selected = selected[0];

            // Not all pickerField's have a collapse API, i.e. Ext.ux.form.MultiSelect.
            if (selected && field.isEqual(record.get(valueField), selected.get(valueField)) &&
                field.collapse) {
                field.collapse();
            }
        }
    },

    onContainerClick: function(e) {
        var toolbar = this.pagingToolbar,
            clientRegion;

        // Ext.view.View template method
        // Do not continue to process the event as a container click
        // if it is within the pagingToolbar
        if (toolbar && toolbar.rendered && e.within(toolbar.el)) {
            return false;
        }

        // IE10 and IE11 will fire pointer events when user drags listWrap scrollbars,
        // which may result in selection being reset.
        if (Ext.isIE10 || Ext.isIE11) {
            clientRegion = this.listWrap.getClientRegion();

            if (!e.getPoint().isContainedBy(clientRegion)) {
                return false;
            }
        }
    },

    doDestroy: function() {
        this.pagingToolbar = Ext.destroy(this.pagingToolbar);

        this.callParent();
    },

    privates: {
        /**
         * @method getNodeContainer
         * @private
         * @inheritdoc
         */
        getNodeContainer: function() {
            return this.listEl;
        },

        getTargetEl: function() {
            return this.listEl;
        },

        getOverflowEl: function() {
            return this.listWrap;
        },

        // Do the job of a container layout at this point even though we are not a Container.
        finishRenderChildren: function() {
            var toolbar = this.pagingToolbar;

            this.callParent(arguments);

            if (toolbar) {
                toolbar.finishRender();
            }
        }
    }
});
